package com.jyh.blog.service.impl;

import cn.hutool.core.exceptions.ExceptionUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jyh.blog.dao.ArticleDao;
import com.jyh.blog.dao.ArticleTagDao;
import com.jyh.blog.dao.CategoryDao;
import com.jyh.blog.dao.TagDao;
import com.jyh.blog.dto.*;
import com.jyh.blog.entity.Article;
import com.jyh.blog.entity.ArticleTag;
import com.jyh.blog.entity.Category;
import com.jyh.blog.entity.Tag;
import com.jyh.blog.enums.FileExtEnum;
import com.jyh.blog.enums.FilePathEnum;
import com.jyh.blog.exception.MyException;
import com.jyh.blog.service.ArticleService;
import com.jyh.blog.service.ArticleTagService;
import com.jyh.blog.service.BlogInfoService;
import com.jyh.blog.service.RedisService;
import com.jyh.blog.strategy.context.ArticleImportStrategyContext;
import com.jyh.blog.strategy.context.ArticleSearchStrategyContext;
import com.jyh.blog.strategy.context.UploadStrategyContext;
import com.jyh.blog.utils.BeanCopyUtils;
import com.jyh.blog.utils.CommonUtils;
import com.jyh.blog.utils.PageUtils;
import com.jyh.blog.utils.UserUtils;
import com.jyh.blog.vo.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpSession;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;

import static com.jyh.blog.constant.CommonConst.ARTICLE_SET;
import static com.jyh.blog.constant.CommonConst.FALSE;
import static com.jyh.blog.constant.RedisPrefixConst.*;
import static com.jyh.blog.enums.ArticleStatusEnum.PUBLIC;

/**
 * (Article)表服务实现类
 *
 * @author jyh
 * @since 2023-02-01 15:25:22
 */
@Service("articleService")
@Slf4j
public class ArticleServiceImpl extends ServiceImpl<ArticleDao, Article> implements ArticleService {
    @Autowired
    private ArticleDao articleDao;
    @Autowired
    private CategoryDao categoryDao;
    @Autowired
    private TagDao tagDao;
    @Autowired
    private ArticleTagDao articleTagDao;
    @Autowired
    private ArticleTagService articleTagService;
    @Autowired
    private BlogInfoService blogInfoService;
    @Autowired
    private ArticleImportStrategyContext articleImportStrategyContext;
    @Autowired
    private UploadStrategyContext uploadStrategyContext;
    @Autowired
    private RedisService redisService;
    @Autowired
    private HttpSession session;
    @Autowired
    private ArticleSearchStrategyContext articleSearchStrategyContext;

    @Override
    public PageResult<ArticleBackDTO> articleBackList(ConditionVO condition) {
        //查询总文章数
        Integer count = articleDao.countBackArticle(condition);
        //条件查询
        List<ArticleBackDTO> records = articleDao.articleBackList(PageUtils.getLimitCurrent(), PageUtils.getSize(), condition);
        //文章浏览量
        Map<Object, Double> viewCount = redisService.zAllScore(ARTICLE_VIEWS_COUNT);
        //文章点赞量
        Map<String, Object> likeMap = redisService.hGetAll(ARTICLE_LIKE_COUNT);
        //封装
        records.forEach(item->{
            Double views = viewCount.get(item.getId());
            if(Objects.nonNull(views)){
                item.setViewsCount(views.intValue());
            }
            Object likeNum = likeMap.get(item.getId());
            item.setLikeCount((Integer) likeNum);
        });

        return new PageResult(records, count);
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public void saveOrUpdateArticle(ArticleVO articleVO) {
        Article article = BeanCopyUtils.copyObject(articleVO, Article.class);
        //获取博客配置信息
        CompletableFuture<WebsiteConfigVO> websiteConfig = CompletableFuture.supplyAsync(() -> blogInfoService.getWebSiteConfig());
        //校验分类名是否存在,草稿无分类名
        Category category = categoryDao.selectOne(new LambdaQueryWrapper<Category>().eq(Category::getName, articleVO.getCategoryName()));
        if (Objects.nonNull(category)) {
            //存在分类，不是草稿，则设置
            article.setCategoryId(category.getId());
        }
        //获取作者id--登录用户
        article.setUserId(UserUtils.getLoginUser().getUserInfoId());
        //对文章设置默认封面
        if (StringUtils.isBlank(article.getCover())) {
            try {
                article.setCover(websiteConfig.get().getArticleCover());
            } catch (Exception e) {
                throw new MyException("设置文章默认封面失败");
            }
        }

        this.saveOrUpdate(article);
        //保存文章标签
        saveTag(articleVO, article.getId());
    }

    @Override
    public ArticleVO getBackArticleById(String articleId) {
        //获取文章
        Article article = articleDao.selectById(articleId);
        //根据文章id获取分类
        Category category = categoryDao.selectById(article.getCategoryId());
        String categoryName = null;
        //如果是文章类型是草稿，可能没有分类id，故查不到分类名
        if (!ObjectUtils.isEmpty(category)) {
            categoryName = category.getName();
        }
        //查询文章的所有标签
        List<String> tagNameList = tagDao.tagNameByArticleId(articleId);
        ArticleVO articleVO = BeanCopyUtils.copyObject(article, ArticleVO.class);
        articleVO.setCategoryName(categoryName);
        articleVO.setTagNameList(tagNameList);
        return articleVO;
    }

    @Override
    public void logicDelete(DeleteVO deleteVO) {
        List<Article> articleList = deleteVO.getIdList().stream().map(id -> Article.builder()
                .id(id)
                .isTop(FALSE)
                .isDelete(deleteVO.getIsDelete())
                .build()).collect(Collectors.toList());
        this.updateBatchById(articleList);
    }

    @Override
    public void deleteArticle(List<String> idList) {
        //删除文章标签
        articleTagDao.delete(new LambdaQueryWrapper<ArticleTag>().in(ArticleTag::getArticleId, idList));
        //删除文章
        this.removeByIds(idList);
    }

    @Override
    public void changTop(ArticleTopVO articleTopVO) {
        Article article = Article.builder().id(articleTopVO.getId()).isTop(articleTopVO.getIsTop()).build();
        this.updateById(article);
    }

    @Override
    public void importArticle(MultipartFile file, String type) {
        articleImportStrategyContext.executeArticleImportStrategy(file, type);
    }

    @Override
    public List<String> exportArticle(List<String> idList) {
        //查询文章
        List<Article> articleList = this.list(new LambdaQueryWrapper<Article>().in(Article::getId, idList));
        ArrayList<String> urlList = new ArrayList<>();
        //上传到OSS
        for (Article article : articleList) {
            try (ByteArrayInputStream inputStream = new ByteArrayInputStream(article.getContent().getBytes())) {
                String url = uploadStrategyContext
                        .executeUploadStrategy(article.getTitle() + FileExtEnum.MD.getExtName(), FilePathEnum.MD.getPath(), inputStream);
                urlList.add(url);
            } catch (IOException e) {
                throw new MyException("导出文章失败");
            }

        }
        return urlList;
    }

    //首页文章列表
    @Override
    public List<ArticleHomeDTO> articleHomeDTOList() {
        return articleDao.articleHomeDTOList(PageUtils.getLimitCurrent(), PageUtils.getSize());
    }

    //首页文章内容
    @Override
    public ArticleDTO articleDetails(String id) {
        //根据id查询文章
        ArticleDTO article = articleDao.getArticleById(id);
        if (Objects.isNull(article)) {
            throw new MyException("文章id错误");
        }
        //查询推荐文章
        CompletableFuture<List<ArticleRecommendDTO>> recommendList = CompletableFuture.supplyAsync(() -> articleDao.recommendArticleList(id));
        //查询最新文章
        CompletableFuture<List<ArticleRecommendDTO>> newArticle = CompletableFuture.supplyAsync(() -> {
            List<Article> articleList = this.list(new LambdaQueryWrapper<Article>()
                    .select(Article::getId, Article::getCover, Article::getTitle, Article::getCreateTime)
                    .eq(Article::getStatus, PUBLIC.getStatus())
                    .eq(Article::getIsDelete, FALSE)
                    .orderByDesc(Article::getId)
                    .last("limit 6")
            );
            return BeanCopyUtils.copyList(articleList, ArticleRecommendDTO.class);
        });
        //更新文章浏览量
        updateArticleViewsCount(id);
        //查询上下篇文章
        Article preArticle = this.getOne(new LambdaQueryWrapper<Article>().select(Article::getId, Article::getCover, Article::getTitle)
                .eq(Article::getStatus, PUBLIC.getStatus()).eq(Article::getIsDelete, FALSE)
                .lt(Article::getId, id)
                .orderByDesc(Article::getId)
                .last("limit 1")
        );
        Article nextArticle = this.getOne(new LambdaQueryWrapper<Article>().select(Article::getId, Article::getCover, Article::getTitle)
                .eq(Article::getStatus, PUBLIC.getStatus()).eq(Article::getIsDelete, FALSE)
                .gt(Article::getId, id)
                .orderByAsc(Article::getId)
                .last("limit 1")
        );
        article.setLastArticle(BeanCopyUtils.copyObject(preArticle, ArticlePaginationDTO.class));
        article.setNextArticle(BeanCopyUtils.copyObject(nextArticle, ArticlePaginationDTO.class));
        //封装点赞量、浏览量
        article.setLikeCount((Integer) redisService.hGet(ARTICLE_LIKE_COUNT, id));
        Double score = redisService.zScore(ARTICLE_VIEWS_COUNT, id);
        if (Objects.nonNull(score)) {
            article.setViewsCount(score.intValue());
        }
        //封装文章信息
        try {
            article.setNewestArticleList(newArticle.get());
            article.setRecommendArticleList(recommendList.get());
        } catch (Exception e) {
            log.error("堆栈信息:{}", ExceptionUtil.stacktraceToString(e));
        }
        return article;
    }

    @Override
    public ArticlePreviewListDTO articleByCondition(ConditionVO conditionVO) {
        //查询预览文章
        List<ArticlePreviewDTO> articlePreviewList = articleDao.articlePreviewList(PageUtils.getLimitCurrent(), PageUtils.getSize(), conditionVO);
        String name;
        if (Objects.nonNull(conditionVO.getCategoryId())) {
            name = categoryDao.selectOne(new LambdaQueryWrapper<Category>()
                    .select(Category::getName)
                    .eq(Category::getId, conditionVO.getCategoryId())).getName();
        } else {
            name = tagDao.selectOne(new LambdaQueryWrapper<Tag>()
                    .select(Tag::getName)
                    .eq(Tag::getId, conditionVO.getTagId())).getName();
        }
        return ArticlePreviewListDTO.builder()
                .articlePreviewDTOList(articlePreviewList)
                .name(name)
                .build();
    }

    /**
     * set：key为用户信息id，集合元素为点赞的文章id
     * hash：key为 ARTICLE_LIKE_COUNT，field为文章id，value为点赞量
     * @param articleId
     */
    @Override
    public void articleLike(String articleId) {
        String key = ARTICLE_USER_LIKE + UserUtils.getLoginUser().getUserInfoId();
        if(redisService.sIsMember(key,articleId)){
            //已点赞，用户再次点击为取消点赞
            redisService.sRemove(key,articleId);
            redisService.hDecr(ARTICLE_LIKE_COUNT,articleId,1L);
        }else{
            //未点赞
            redisService.sAdd(key,articleId);
            redisService.hIncr(ARTICLE_LIKE_COUNT,articleId,1L);
        }
    }

    @Override
    public PageResult<ArchiveDTO> archives() {
        //分页查询
        Page<Article> page=new Page<>(PageUtils.getLimitCurrent(),PageUtils.getSize());
        Page<Article> articlePage = this.page(page, new LambdaQueryWrapper<Article>()
                .select(Article::getId, Article::getTitle, Article::getCreateTime)
                .eq(Article::getIsDelete, FALSE)
                .eq(Article::getStatus, PUBLIC.getStatus()));
        List<ArchiveDTO> archiveDTOList = BeanCopyUtils.copyList(articlePage.getRecords(), ArchiveDTO.class);
        return new PageResult<>(archiveDTOList,(int)articlePage.getTotal());
    }

    @Override
    public List<ArticleSearchDTO> articleSearchList(ConditionVO conditionVO) {
        return articleSearchStrategyContext.executeArticleSearch(conditionVO.getKeywords());
    }

    /**
     * 更新文章浏览量
     * @param id
     */
    private void updateArticleViewsCount(String id) {
        //第一次访问时，增加访问数
        //从session中获取文章访问列表（存的文章id），为空则新建一个HashSet
        Set<String> viewSet = CommonUtils.castSet(Optional.ofNullable(session.getAttribute(ARTICLE_SET)).orElseGet(HashSet::new), String.class);
        if (!viewSet.contains(id)) {
            //将访问集合添加session
            viewSet.add(id);
            session.setAttribute(ARTICLE_SET, viewSet);
            //浏览量自增
            redisService.zIncr(ARTICLE_VIEWS_COUNT, id, 1D);
        }

    }

    private void saveTag(ArticleVO articleVO, String articleId) {
        //article_tag表中删除原文章id对应的标签记录
        //条件成立：传入文章有id，删除article_tag中对应记录
        //草稿类型，有id，传入此方法前保存了
        if (!ObjectUtils.isEmpty(articleVO.getId())) {
            articleTagDao.delete(new LambdaQueryWrapper<ArticleTag>().eq(ArticleTag::getArticleId, articleVO.getId()));
        }
        //草稿时无标签
        List<String> tagNameList = articleVO.getTagNameList();
        //不是草稿时
        if (!ObjectUtils.isEmpty(tagNameList)) {
            //校验标签名是否存在
            List<Tag> tagList = tagDao.selectList(new LambdaQueryWrapper<Tag>().in(Tag::getName, tagNameList));
            if (tagNameList.size() == tagList.size()) {
                //取出标签id
                List<String> tagIdList = tagList.stream().map(Tag::getId).collect(Collectors.toList());

                List<ArticleTag> articleTagList = tagIdList.stream().map(item -> ArticleTag.builder()
                        .articleId(articleId)
                        .tagId(item)
                        .build())
                        .collect(Collectors.toList());

                articleTagService.saveBatch(articleTagList);
            }
        }

    }
}

